package com.gitee.wsl.net.ssdp.server

import com.gitee.wsl.api.closeQuietly
import com.gitee.wsl.ext.net.isAvailableV4Interface
import com.gitee.wsl.logger.Logger
import com.gitee.wsl.logger.debug
import com.gitee.wsl.logger.error
import com.gitee.wsl.logger.info
import com.gitee.wsl.net.ssdp.SSDP_ADDRESS_V4
import com.gitee.wsl.net.ssdp.SSDP_ADDRESS_V6
import com.gitee.wsl.net.ssdp.SSDP_PORT
import com.gitee.wsl.net.ssdp.message.Headers
import com.gitee.wsl.net.ssdp.message.SsdpMessage
import com.gitee.wsl.net.ssdp.message.SsdpMessageType
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.isActive
import java.io.IOException
import java.net.*
import kotlin.coroutines.CoroutineContext

/**
 * Implements common [ISsdpServer] functionality
 * @param networkInterface  - network interface to bind server to
 * @param bindPort          - port number
 * @param address           - multicast address
 * @param coroutineContext  - context to use
 * @param cpName            - name of the control point for CPFN.UPNP.ORG header
 */
class SsdpServerDelegate(
    private val address: InetAddress,
    override val networkInterface: NetworkInterface,
    private val bindPort: Int = 0,
    private val receivingChannel: SendChannel<SsdpMessage>,
    override val cpName: String,
    override val coroutineContext: CoroutineContext,
) : ISsdpServer{
    private var socket: MulticastSocket? = null
    private var loop: Job? = null
    private var inboundFilter: (SsdpMessage) -> Boolean = { true }         // accept all

    override fun setFilter(filter: ((SsdpMessage) -> Boolean)?) {
        inboundFilter = filter
            ?: { !(it.type == SsdpMessageType.Search && it.getHeader(Headers.ControlPointFriendlyName) == cpName) }
    }

    override fun start(): ISsdpServer? {
        if (!this.coroutineContext.isActive) return null
        Logger.debug("Starting SSDP server on interface: ${networkInterface.displayName}")

        try {
            val socket = createMulticastSocket(bindPort)
            this.socket = socket
            if (bindPort != 0) {
                socket.joinGroup(getSsdpSocketAddress(socket.networkInterface), socket.networkInterface)
            }
            receiveLoop(socket)
        } catch (ex: Exception) {
            Logger.error("Cannot start SSDP server", ex)
            this.socket?.closeQuietly()
            this.socket = null
        }
        return this
    }

    @Throws(IOException::class)
    internal fun createMulticastSocket(port: Int) = MulticastSocket(port).also {
        it.networkInterface = networkInterface
        it.timeToLive = 4
    }

    @Throws(IOException::class)
    internal fun receiveLoop(socket: MulticastSocket) {
        this.loop = launch(coroutineContext) {
            val buf = ByteArray(1024)
            while (this.coroutineContext.isActive) {
                try {
                    val dp = DatagramPacket(buf, buf.size)
                    socket.receive(dp)
                    Logger.debug("Received UDP packet from ${dp.address}:${dp.port}")
                    Logger.info(dp.data.decodeToString())
                    val msg = SsdpMessage.fromDatagram(dp)
                    if (inboundFilter.invoke(msg)) {
                        receivingChannel.send(msg)
                    }
                } catch (ex: Exception) {
                    Logger.error("Failed to receive UDP packet", ex)
                }
            }
        }
    }

    override fun stop() {
        this.socket?.closeQuietly()
        this.socket = null
        this.loop?.cancel()
    }

    override fun send(message: SsdpMessage, address: InetAddress?, port: Int?) {
        val payload = message.asByteArray()
        this.socket?.send(
            DatagramPacket(
                payload,
                payload.size,
                address ?: this.address,
                port ?: bindPort
            )
        )
    }

    override fun isActive() = this.loop?.isActive == true
}


fun getSsdpAddress(ni: NetworkInterface): InetAddress = if(ni.isAvailableV4Interface()){
    Inet4Address.getByName(SSDP_ADDRESS_V4)
} else {
    Inet6Address.getByName(SSDP_ADDRESS_V6)
}

fun getSsdpSocketAddress(ni: NetworkInterface): SocketAddress
    = InetSocketAddress(getSsdpAddress(ni), SSDP_PORT)

//fun getEventAddress(ni: NetworkInterface): InetAddress {
//    return if (ni.isAvailableV4Interface()) {
//        Inet4Address.getByName(EVENT_ADDRESS_V4)
//    } else {
//        Inet6Address.getByName(EVENT_ADDRESS_V6)
//    }
//}